#!/usr/bin/env python
# -*- coding: UTF-8 -*-
### Copyright (C) 2009 Kelvin Ho <kelvin.ho.ka.wai@gmail.com>
###
### This library is free software; you can redistribute it and/or
### modify it under the terms of the GNU General Public License as
### published by the Free Software Foundation; either version 2 of the
### License, or (at your option) any later version.
###
### This library is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
### General Public License for more details.
###
### You should have received a copy of the GNU General Public License
### along with this library; if not, write to the Free Software
### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
### 02111-1307, USA.

import re
import os
import sys

import gedit
import gobject
import gtk

import rikai as RC
import debug
from pangobuffer import PangoBuffer

def _(s): return s
D = debug.debug
I = debug.info

PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
CONF_FN = os.path.join(PLUGIN_DIR, 'rikai.conf')

def get_uniq_str(doc):
  s = str(doc)
  return re.search(r"\([^)]+\)", s).group()

def highlight_buffer(buffer, start_offset, end_offset):
  '''Removes previous highlight and applies highlight at
  new location.'''
  tagtable = buffer.get_tag_table()
  htag = tagtable.lookup('highlight')
  if not htag:
    D('highlight tag not found, creating tag')
    htag = gtk.TextTag('highlight')
    htag.set_property('background', '#00ee33')
    htag.set_property('background-set', True)
    htag.set_property('background-full-height', True)
    htag.set_property('background-full-height-set', True)
    tagtable.add(htag)
  buffer.remove_tag_by_name('highlight', buffer.get_start_iter(),\
                        buffer.get_end_iter())

  start_iter = buffer.get_iter_at_offset(start_offset)
  end_iter = buffer.get_iter_at_offset(end_offset)
  buffer.apply_tag(htag, start_iter, end_iter)


class Config(object):
  '''Very simple (crude, even) config class.'''
  DEFAULT = {
    'StartRikaiAtStart': 'f',
    'DebugLevel': 'D',
    'Lookahead': '8',
    'ActivationMethod': 'selection'
  }
  def __init__(self, fn):
    self._fn = fn
    self._items = {}
    if os.path.exists(fn):
      self.load()
    else:
      self._init_options()

  def _init_options(self):
    for key, val in self.DEFAULT.iteritems():
      self[key] = val

  def __setitem__(self, i, v):
    self._items[i] = str(v)

  def __getitem__(self, i):
    val = self._items[i]
    if val in ['True', 'true', 't', 'T']:
      return True
    elif val in ['False', 'false', 'f', 'F']:
      return False
    elif val.isdigit():
      return int(val)
    else:
      return val
     
  def write(self):
    fn = self._fn
    f = open(fn, 'w')
    for k, v in self._items.iteritems():
      f.write('%s=%s\n' % (k, str(v)))
    f.close()

  def load(self):
    fn = self._fn
    f = open(fn, 'r')
    for line in f:
      if '#' in line: line = line[:line.find('#')]
      line = line.rstrip('\n')
      if not line: continue
      k, v = line.split('=')
      k = k.lstrip(' ').rstrip(' ')
      v = v.lstrip(' ').rstrip(' ')
      self._items[k] = v
    f.close()

    for k, v in self.DEFAULT.iteritems():
      if k not in self._items:
        self[k] = v
    
CONFIG = Config(CONF_FN)
debug.set_debug_level('I')

class RikaiPanel(gtk.VBox):
  __gsignals__ = {
    "selection-changed": (
        gobject.SIGNAL_RUN_LAST,
        None,
        (gobject.TYPE_PYOBJECT,)*2),
    "mouse-moved": (
        gobject.SIGNAL_RUN_LAST,
        None,
        (gobject.TYPE_PYOBJECT,)*3),
  }
  def __init__(self):
    gtk.VBox.__init__(self)

    self._buf = PangoBuffer()
    self._view = gtk.TextView(self._buf)
    self._view.set_editable(False)
    self._view.set_cursor_visible(False)
    self._view.set_wrap_mode(gtk.WRAP_WORD_CHAR)

    self._lookupbar_entry = gtk.Entry()
    self._lookupbar_search_btn = gtk.Button('Search')
    self._lookupbar_search_btn.connect('clicked', self.clicked)
    self._lookupbar_entry.connect('activate', self.clicked)

    self._lookupbar = gtk.HBox()
    self._lookupbar.pack_start(self._lookupbar_entry, True, True)
    self._lookupbar.pack_start(self._lookupbar_search_btn, False, False)

    scr = gtk.ScrolledWindow()
    scr.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
    scr.add_with_viewport(self._view)

    self.pack_start(scr, True, True)
    self.pack_start(self._lookupbar, False, False)

    self.connect('selection-changed', self.selection_changed)
    self.connect('mouse-moved', self.mouse_moved)

  def format_entry(self, entry):
    kanji = u'<span foreground="#0033DD" weight="heavy">%s</span>' % \
              entry.kanji
    furigana = u'<span foreground="#1166FF">%s</span>' % entry.furigana \
                  if entry.furigana else ''
    exp = re.sub(r'(\(\d\))', \
                r'<span weight="bold">\1</span>', u'; '.join(entry.exps))
    exp = exp.replace(u'&', u'&amp;')
    return '%s%s%s\n%s\n' % (kanji, ' '*2, \
                furigana, exp)

  def get_entries(self, text):
    entries = RC.rikai(text, minentrylen=4)
    return entries

  def update_panel(self, entries):
    self._buf.set_text(' ')
    entry_str = '\n'.join(self.format_entry(entry) for entry in entries)
    self._buf.set_text('<span font_family="monospace">'+entry_str+'</span>')

  def selection_changed(self, widget, doc, text):
    if CONFIG['ActivationMethod'] == 'selection':
      D('selection changed')
      entries = self.get_entries(text)
      self.update_panel(entries)

  def mouse_moved(self, widget, doc, startend, text):
    if CONFIG['ActivationMethod'] == 'mouse_motion':
      D('mouse moved')
      entries = self.get_entries(text)
      if entries:
        kanji = entries[0].kanji
        furi = entries[0].furigana
        start = startend[0]
        l = len(kanji) if text.startswith(kanji) or not furi else len(furi)
        highlight_buffer(doc, start, start+l)
        self.update_panel(entries)

  def clicked(self, btn):
    text = self._lookupbar_entry.get_text()
    text = u"%s" % text
    entries = self.get_entries(text)
    self.update_panel(entries)

class RikaiHelper(object):
  def __init__(self, window):
    self._window = window
    self._panel = RikaiPanel()
    self._panel.show_all()

    bottom = self._window.get_bottom_panel()
    bottom.add_item(self._panel, _("Rikai"), \
              gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU))

    RC.init()

    stat = self._window.get_statusbar()
    self._context_id = stat.get_context_id('rikai')

    stat.push(self._context_id, _('Rikai loaded.'))

    self._docs = []
    self._views = []

    self._sel_start_iter = None
    self._sel_end_iter = None
    self._last_iter = None
    self._curr_doc = None
     
    self._callbacks = []

  def connect_callback(self, widget, evt_name, cb, *args):
    handler_id = widget.connect(evt_name, cb, *args)
    D('Connected callback with id=%d' % handler_id)
    self._callbacks.append((widget, handler_id))

  def remove_callbacks(self):
    for widget, id in self._callbacks:
      if widget.handler_is_connected(id):
        widget.disconnect(id)
    self._callbacks = []

  def mark_set(self, document, iter, mark):
    '''Checks to see if there is a selection, and if the insert/selection 
    bound marks in the textbuffer have moved. If both conditions are true
    there has been a change in the selection.'''
    if document.get_property('has-selection'):
      start = document.get_insert()
      end = document.get_selection_bound()
      start_iter = document.get_iter_at_mark(start)
      end_iter = document.get_iter_at_mark(end)
      
      start_iter_changed = False
      end_iter_changed = False
      curr_doc_changed = False

      if not self._curr_doc or self._curr_doc != document or \
          not self._sel_start_iter or \
          not self._sel_start_iter.equal(start_iter) or \
          not self._sel_end_iter or \
          not self._sel_end_iter.equal(end_iter):
        text = document.get_text(start_iter, end_iter)
        text = u"%s" % text
        text = text.replace(u"　", u"").replace(u"\n", u"")
        if text:
          start_iter_changed = True
          end_iter_changed = True
          curr_doc_changed = True
          print 'selection changed'
          self._panel.emit('selection-changed', \
                  document,\
                  text)
      if start_iter_changed: self._sel_start_iter = start_iter
      if end_iter_changed: self._sel_end_iter = end_iter
      if curr_doc_changed: self._curr_doc = document

  
  def motion_notify(self, view, event):
    x, y = int(event.x), int(event.y)
    buf_x, buf_y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
    iter = view.get_iter_at_location(buf_x, buf_y)
    if not self._last_iter or not self._last_iter.equal(iter):
      self._last_iter = iter
      buffer = view.get_buffer()
      lookahead = CONFIG['Lookahead']

      linelen = iter.get_chars_in_line()-1
      start_index = iter.get_line_offset()
      end_index = start_index+lookahead if \
                    start_index+lookahead < linelen else linelen
      if start_index >= 0 and end_index >= 0:
        iter2 = buffer.get_iter_at_line_offset(iter.get_line(), end_index)
         
        text = buffer.get_text(iter, iter2)
        text = u"%s" % text
        text = text.replace(u"　", u"").replace(u"\n", u"")
        #D(text)
        if text:
          self._panel.emit('mouse-moved', buffer, \
                      (iter.get_offset(), iter2.get_offset()), text)

  def deactivate(self):
    D('deactivate')
    RC.shutdown()
    self._window.get_bottom_panel().remove_item(self._panel)
    self._window.get_statusbar().push(self._context_id, _('Rikai shut down.'))
    self.remove_callbacks()
     
  def update_ui(self):
    D('update_ui')
    docs = self._window.get_documents()
    views = self._window.get_views()
    self.register_documents(docs)
    self.register_views(views)

  def register_documents(self, docs):
    for doc in docs:
      doc_str = get_uniq_str(doc)
      if doc_str not in self._docs:
        D('register %s' %doc_str)
        #doc.connect('mark-set', self.mark_set)
        self.connect_callback(doc, 'mark-set', self.mark_set)
        self._docs.append(doc_str)

  def register_views(self, views):
    for view in views:
      view_str = get_uniq_str(view)
      if view_str not in self._views:
        D('register %s' % view_str)
        #view.connect('motion-notify-event', self.motion_notify)
        self.connect_callback(view, 'motion-notify-event', self.motion_notify)
        self._views.append(view_str)

class RikaiPlugin(gedit.Plugin):
  RIKAI_KEY = "RikaiPluginData"

  UI_STR = """
    <ui>
      <menubar name="MenuBar">
        <menu name="ToolsMenu" action="Tools">
          <placeholder name="ToolsOps_1">
            <menuitem name="rikai_start" action="rikai_start"/>
            <menuitem name="rikai_stop" action="rikai_stop"/>
          </placeholder>
        </menu>
      </menubar>
    </ui>
  """

  def __init__(self):
    gedit.Plugin.__init__(self)
    self._started = False
     
    self._dialog = gtk.Dialog('PyRikai Configuration', \
                      None, \
                      gtk.DIALOG_MODAL, \
                      (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, \
                      gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
    self._dialog.connect('response', self.on_dialog_response)
    cont_area = self._dialog.get_content_area()
     
    self._rikai_start_btn = gtk.CheckButton(\
                    'Start PyRikai when gedit _starts')
    cont_area.pack_start(self._rikai_start_btn, False, False)


  def on_dialog_response(self, dialog, resp_id):
    dialog.hide()
    if resp_id == gtk.RESPONSE_ACCEPT:
      CONFIG['StartRikaiAtStart'] = str(self._rikai_start_btn.get_active())

  def activate(self, window):
    D('activate')
    self._window = window
    self.add_menu_item(window)
    if CONFIG['StartRikaiAtStart']:
      self.start_rikai(None)

  def deactivate(self, window):
    D('deactivate')
    self.stop_rikai(None)
    self.remove_menu_item(window)
    CONFIG.write()

  def update_ui(self, window):
    D('update ui')
    h = window.get_data(self.RIKAI_KEY)
    if h:
      h.update_ui()

  def is_configurable(self):
    return True

  def create_configure_dialog(self):
    D('create dialog')
    self._rikai_start_btn.set_active(CONFIG['StartRikaiAtStart'])
    self._dialog.show_all()
    return self._dialog

  def add_menu_item(self, window):
    manager = window.get_ui_manager()
    self._action_group = gtk.ActionGroup("Rikai")
     
    self._start_action = gtk.Action('rikai_start', \
                  _('Start PyRikai'), _('Start PyRikai'), gtk.STOCK_NEW)
    self._stop_action = gtk.Action('rikai_stop', \
                  _('Stop PyRikai'), _('Stop PyRikai'), gtk.STOCK_NEW)
    self._start_action.set_visible(not self._started) 
    self._start_action.connect('activate', self.start_rikai)
    self._stop_action.set_visible(self._started) #depends on config, really
    self._stop_action.connect('activate', self.stop_rikai)
     
    self._action_group.add_action(self._start_action)
    self._action_group.add_action(self._stop_action)

    manager.insert_action_group(self._action_group, -1)
    self._ui_id = manager.add_ui_from_string(self.UI_STR)

  def remove_menu_item(self, window):
    manager = window.get_ui_manager()
    if hasattr(self, '_ui_id'):
      manager.remove_ui(self._ui_id)
      manager.remove_action_group(self._action_group)
      #manager.ensure_update()

  def start_rikai(self, action):
    if self._started: return
    D('starting rikai')
    helper = RikaiHelper(self._window)
    helper.update_ui()
    self._window.set_data(self.RIKAI_KEY, helper)
    self._started = True
    self._start_action.set_visible(False)
    self._stop_action.set_visible(True)

  def stop_rikai(self, action):
    if not self._started: return
    D('stopping rikai')
    self._window.get_data(self.RIKAI_KEY).deactivate()
    self._window.set_data(self.RIKAI_KEY, None)
    self._started = False
    self._start_action.set_visible(True)
    self._stop_action.set_visible(False)
    
